feat: add grok agent (official xAI Grok CLI) - #404
Conversation
There was a problem hiding this comment.
Pull request overview
Adds Grok as a first-party agent in deva by integrating xAI’s official (closed-source) Grok CLI (@xai-official/grok) with pinned versioning, auth-mode wiring, and a safeguard against ~/.grok/bin host shadowing the in-image binary.
Changes:
- Introduces a new
grokagent module withoauth(default) andapi-key(XAI_API_KEY) auth modes and--always-approve. - Pins
GROK_CLI_VERSIONand wires it through build args/labels, version tooling, and CI/nightly/release workflows. - Updates install tooling to “pin” the Grok platform binary into
~/.local/binand remove~/.grok/binto avoid host-mounted binary shadowing.
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
versions.env |
Adds GROK_CLI_VERSION pin. |
tests/version-upgrade.sh |
Extends version-upgrade test mocks/assertions to include Grok. |
tests/test_release_utils.sh |
Updates tool registry expectations to include grok-cli. |
scripts/version-upgrade.sh |
Adds Grok to version resolution, manifest output, and docker build args. |
scripts/version-pins.sh |
Adds GROK_CLI_VERSION to the pin variable list. |
scripts/update-version-pins.sh |
Fetches/refreshes GROK_CLI_VERSION from npm. |
scripts/test-version-targets.sh |
Extends version-target tests to cover GROK_CLI_VERSION. |
scripts/test-install-agent-tooling.sh |
Adds Grok stubs and assertions for the pin_grok_platform_binary behavior. |
scripts/resolve-tool-versions.sh |
Resolves grok_cli_version via registry tooling. |
scripts/release-utils.sh |
Adds grok-cli to the tool registry and display names. |
scripts/install-agent-tooling.sh |
Installs @xai-official/grok and pins the platform binary to prevent host shadowing. |
README.md |
Documents Grok as a supported agent and adds invocation examples. |
Makefile |
Adds GROK_CLI_VERSION build arg plumbing and help text. |
install.sh |
Includes agents/grok.sh in installed agent files. |
docs/quick-start.md |
Adds Grok to the installed files list and shows API-key usage example. |
docs/how-it-works.md |
Documents .grok canonical entry and config-home layouts. |
docs/authentication.md |
Documents Grok auth modes and the .grok/bin binary-shadowing mitigation. |
docs/advanced-usage.md |
Updates deva-root layout examples to include grok/. |
Dockerfile.rust |
Adds GROK_CLI_VERSION ARG and image label. |
Dockerfile |
Adds GROK_CLI_VERSION ARG and image label for main image. |
deva.sh |
Adds .grok canonical wiring, autolink, env filtering, and auth overlay target for Grok. |
DEV-LOGS.md |
Records a dev log entry describing the Grok integration and rationale. |
CHANGELOG.md |
Adds an Unreleased entry for the new grok agent feature. |
agents/shared_auth.sh |
Enables Grok auth parsing and default auth method mapping. |
agents/grok.sh |
New agent definition with oauth/api-key auth wiring and --always-approve. |
.github/workflows/release.yml |
Wires Grok version output and build args into release workflow. |
.github/workflows/nightly-images.yml |
Wires Grok version resolution and build args into nightly workflow. |
.github/workflows/ci.yml |
Adds deva.sh grok -Q -- --version to CI smoke checks and passes Grok build arg. |
.deva.example |
Updates hybrid config-home documentation to include Grok. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 59419988de
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| grok) | ||
| # grok prefers the auth.json session token over XAI_API_KEY, so the | ||
| # blank overlay is what makes api-key auth win over a mounted ~/.grok. | ||
| printf '%s' "/home/deva/.grok/auth.json" |
There was a problem hiding this comment.
Prevent Grok config keys from overriding api-key auth
In api-key mode with an existing ~/.grok/config.toml that sets a per-model api_key or env_key, this overlay only hides the session file while the rest of the mounted .grok directory remains visible, so Grok can still ignore the XAI_API_KEY passed by deva. xAI's auth docs list credential priority as model.api_key > model.env_key > active session token > XAI_API_KEY, so users trying to switch billing/projects with --auth-with api-key can silently run against the wrong key; also mask/override config credentials or avoid mounting .grok for this mode.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 7ad054b, and you were right — masking auth.json alone was insufficient. api-key mode now mounts no ~/.grok at all (matching #403's original spec: "pass XAI_API_KEY, no auth-dir mount"). Precedence confirmed against docs.x.ai: model.api_key > model.env_key > session token > XAI_API_KEY. The blank auth.json overlay stays as defense for explicit -v mounts. test-mount-shape.sh now asserts zero .grok mounts in api-key mode.
| fetch_npm_version() { | ||
| npm view "$1" version 2>/dev/null || true | ||
| curl -fsSL --max-time 10 \ | ||
| "https://registry.npmjs.org/-/package/$1/dist-tags" 2>/dev/null | \ | ||
| sed -n 's/.*"latest":"\([^"]*\)".*/\1/p' | ||
| } |
There was a problem hiding this comment.
Tested live: the unencoded scoped path returns HTTP 200 with identical output to the %2f form (curl https://registry.npmjs.org/-/package/@xai-official/grok/dist-tags), so the 404 claim doesn't hold. The set -e claim doesn't apply here either: the substitution is passed as an argument to refresh_pin, so a curl failure can't abort the script and the old pin is kept. Moot for this PR regardless — the curl-based fetch was part of the proxy commit, now split out to #407.
| _npm_registry_latest() { | ||
| curl -fsSL --max-time 10 \ | ||
| "https://registry.npmjs.org/-/package/$1/dist-tags" 2>/dev/null | \ | ||
| sed -n 's/.*"latest":"\([^"]*\)".*/\1/p' | ||
| } |
There was a problem hiding this comment.
Half right. Encoding claim is false (tested live, both forms return 200). The failure-handling concern is real though: fetched=$(fetch_latest_version ...) is a plain assignment under set -euo pipefail, so a transient curl failure would abort the report and dead-code the warn-and-fallback branch below it. The curl change moved to #407 and will re-land with || echo "" plus an outage fixture.
| log_proxy_config() { | ||
| local has_proxy=0 | ||
| for var in HTTP_PROXY HTTPS_PROXY http_proxy https_proxy NO_PROXY no_proxy; do | ||
| if [ -n "${!var:-}" ]; then | ||
| log " $var=${!var}" | ||
| has_proxy=1 | ||
| fi | ||
| done | ||
| if [ "$has_proxy" -eq 0 ]; then | ||
| log " (no proxy configured)" | ||
| fi | ||
| } |
There was a problem hiding this comment.
Valid — and broader than flagged: version-upgrade.sh echoes the generated proxy args and make echoes recipes, same exposure. The proxy work is split out to #407; redaction of proxy userinfo across all three sites is in its test plan.
| $(if $(NO_PROXY),--build-arg NO_PROXY=$(NO_PROXY)) \ | ||
| $(if $(no_proxy),--build-arg no_proxy=$(no_proxy)) | ||
|
|
||
| DOCKER_BUILD_FLAGS = $(PROXY_BUILD_ARGS) $(DOCKER_BUILD_EXTRA_ARGS) |
There was a problem hiding this comment.
Valid — one root cause across these four comments: the rewrite needs --add-host host.docker.internal:host-gateway on the docker build side for native Linux Engine (Desktop/OrbStack resolve it implicitly). Proxy work split out to #407, which fixes this once in the shared flags, plus the userinfo-blind rewrite (://user:pass@127.0.0.1 is not matched by the current sed).
| docker build -f "$DOCKERFILE" \ | ||
| ${PROXY_ARGS[@]+"${PROXY_ARGS[@]}"} \ | ||
| --target agent-base \ |
There was a problem hiding this comment.
Valid — one root cause across these four comments: the rewrite needs --add-host host.docker.internal:host-gateway on the docker build side for native Linux Engine (Desktop/OrbStack resolve it implicitly). Proxy work split out to #407, which fixes this once in the shared flags, plus the userinfo-blind rewrite (://user:pass@127.0.0.1 is not matched by the current sed).
| docker build -f "$DOCKERFILE" \ | ||
| ${PROXY_ARGS[@]+"${PROXY_ARGS[@]}"} \ | ||
| --build-arg NODE_MAJOR="$NODE_MAJOR" \ |
There was a problem hiding this comment.
Valid — one root cause across these four comments: the rewrite needs --add-host host.docker.internal:host-gateway on the docker build side for native Linux Engine (Desktop/OrbStack resolve it implicitly). Proxy work split out to #407, which fixes this once in the shared flags, plus the userinfo-blind rewrite (://user:pass@127.0.0.1 is not matched by the current sed).
| docker build -f "$RUST_DOCKERFILE" \ | ||
| ${PROXY_ARGS[@]+"${PROXY_ARGS[@]}"} \ | ||
| --build-arg BASE_IMAGE="$CORE_IMAGE" \ |
There was a problem hiding this comment.
Valid — one root cause across these four comments: the rewrite needs --add-host host.docker.internal:host-gateway on the docker build side for native Linux Engine (Desktop/OrbStack resolve it implicitly). Proxy work split out to #407, which fixes this once in the shared flags, plus the userinfo-blind rewrite (://user:pass@127.0.0.1 is not matched by the current sed).
- agents/grok.sh: oauth default (mount ~/.grok; in-container login via grok login --device-auth) and api-key (XAI_API_KEY); appends --always-approve since the container is the sandbox - deva.sh: .grok canonical entry, autolink, XAI_API_KEY env filtering, blank auth.json overlay for api-key mode (grok prefers the session token over the env key; the overlay makes the key actually win) - install tooling: grok's npm bin is a trampoline that resolves ~/.grok/bin/grok first, and postinstall parks the ~125MB real binary there (self-update dir); move it to ~/.local/bin and drop ~/.grok/bin so a host-mounted ~/.grok (possibly a macOS binary) can never shadow the image CLI - pin GROK_CLI_VERSION=0.2.93 through versions.env, Makefile, Dockerfile(+rust), version scripts, ci/nightly/release workflows, install.sh agent list - tests: registry, version-upgrade, install-tooling, version-targets - docs: README, authentication, quick-start, how-it-works, changelog Close #403 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review hardening for #404 (thanks Codex + two deep review passes): - tmpfs over ~/.grok/bin and ~/.grok/downloads whenever a host dir is mounted at /home/deva/.grok. Reproduced against real 0.2.93: a mounted config.toml without postinstall's `[cli] installer = "npm"` marker flips the updater to installer=internal, and `grok update` re-creates both dirs ON THE HOST MOUNT — a Linux binary that shadows a macOS host CLI via the host npm trampoline. Updater writes now die with the container; verified live (mount stack + host-side clean). - api-key mode mounts no ~/.grok, per the #403 spec. grok credential priority is model.api_key > model.env_key > session token > XAI_API_KEY, so anything a mounted config carries can outrank — and bill instead of — the exported key. Blank auth.json overlay kept as defense for explicit -v mounts. - docker-entrypoint.sh: grok version header, verbose diagnostics, and binary presence check (#403 scope, was missing). - test-mount-shape.sh: grok in hybrid/default loops; new assertions for the update guard and api-key no-mount. - docs/authentication.md, CHANGELOG, DEV-LOGS: correct the false "grok update is a no-op" claim; document the real behavior. Refs #403 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
b6360a6 to
7ad054b
Compare
| - deva.sh append_grok_update_guard: the reverse hole, found in review and reproduced against the real 0.2.93 — a mounted config.toml without the postinstall's `[cli] installer = "npm"` marker flips the updater to installer=internal, and `grok update` re-creates ~/.grok/bin + ~/.grok/downloads ON THE HOST MOUNT (Linux binary shadows a macOS host CLI via the host trampoline). Fix: tmpfs over both dirs whenever a host dir lands at /home/deva/.grok; updater writes die with the container | ||
| - docker-entrypoint.sh: grok version header, verbose diagnostics, ensure_agent_binaries | ||
| - GROK_CLI_VERSION pin wired through versions.env, Makefile, Dockerfile(+rust), version-pins/upgrade/report/resolve scripts, ci/nightly/release workflows; tests extended (release-utils registry, version-upgrade mock, install-tooling, version-targets, mount-shape grok + update-guard + api-key no-mount assertions) | ||
| - Result: deva.sh grok works with both auth modes (dry-run + mount-shape verified); grok pinned at 0.2.93; make versions-up picks up new upstream releases; host ~/.grok cannot be poisoned by in-container updates (tmpfs guard verified live: uid/gid/mode/size + noexec) |
Adds
grokas the fourth first-party agent: xAI's official Grok CLI (x.ai/cli, npm@xai-official/grok, closed source). Not the community superagent-ai/grok-cli (grok-dev) — different product, same bin name; that one stays out.Close #403
What
agents/grok.sh—oauthdefault (mounts~/.grok; first login inside a container viagrok login --device-auth),api-keyalt (XAI_API_KEYfrom console.x.ai). Appends--always-approve(grok's bypassPermissions): container is the sandbox, same as claude/codex/gemini.deva.sh—.grokcanonical entry, autolink~/.grokinto the config root, XAI_API_KEY env filtering per auth mode, default-auth maps.~/.grok(per Add grok agent: integrate official xAI Grok CLI (@xai-official/grok) as fourth first-party agent #403 spec; confirmed by Codex review). grok credential priority ismodel.api_key>model.env_key> session token >XAI_API_KEY, so anything a mounted config carries could outrank — and bill instead of — the exported key. The blankauth.jsonoverlay stays as defense for explicit-vmounts.~/.grok/bin/grokFIRST, and postinstall parks the ~125MB real binary there — inside the very dir we bind-mount for auth.pin_grok_platform_binary()moves the binary to~/.local/bin, repoints the npm bin, removes~/.grok/bin. Outbound (found in review, reproduced against real 0.2.93): a mountedconfig.tomlwithout postinstall's[cli] installer = "npm"marker flips grok's updater toinstaller=internal, andgrok updatere-creates~/.grok/bin+~/.grok/downloadson the host mount — a Linux binary that breaks a macOS host CLI. Fix:append_grok_update_guardoverlays both dirs with container-local tmpfs whenever a host dir is mounted at/home/deva/.grok; updater writes die with the container.docker-entrypoint.sh— grok version header, verbose diagnostics, binary presence check.GROK_CLI_VERSION=0.2.93pinned through versions.env, Makefile, Dockerfile(+rust), version-pins/upgrade/report/resolve scripts, ci/nightly/release workflows, install.sh agent file list.make versions-upand nightly resolve pick up upstream releases via thegrok-cliregistry entry.Tests
.local/bin, npm bin repointed,~/.grok/bingone.grokand absent (plus no.grokmount at all) in api-key modedeva.sh grok -Q -- --versionalongside the other agentsinstall_npm_agent_toolingrun against npmjs on linux/arm64 installs and verifies all four CLIs; updater write-back reproduced and the tmpfs guard verified live against the real docker daemon (mount stack + host dir clean after in-container writes)Notes
b6360a6) is split out to its own issue/PR — it broke Basic Tests (stale fixtures) and has independent gaps (host-gateway mapping, credential logging) that deserve their own review.scripts/version-upgrade.sh_manifest_pairsnever listed Gemini; entrypoint verbose diagnostics also predate gemini (its fallback reports codex). Both worth a small follow-up.grok updatedownloads into container-local tmpfs and cannot touch the host install; the image pin (GROK_CLI_VERSION) is the only version that matters.🤖 Generated with Claude Code